package com.aviary.android.feather.widget;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;

public class ArrayAdapterExtended<T> extends BaseAdapterExtended implements Filterable {

	/**
	 * Contains the list of objects that represent the data of this ArrayAdapter. The content of this list is referred to as
	 * "the array" in the documentation.
	 */
	private List<T> mObjects;

	/**
	 * Lock used to modify the content of {@link #mObjects}. Any write operation performed on the array should be synchronized on
	 * this lock. This lock is also used by the filter (see {@link #getFilter()} to make a synchronized copy of the original array of
	 * data.
	 */
	private final Object mLock = new Object();

	/**
	 * The resource indicating what views to inflate to display the content of this array adapter.
	 */
	private int mResource;

	/**
	 * The resource indicating what views to inflate to display the content of this array adapter in a drop down widget.
	 */
	private int mDropDownResource;

	/**
	 * If the inflated resource is not a TextView, {@link #mFieldId} is used to find a TextView inside the inflated views hierarchy.
	 * This field must contain the identifier that matches the one defined in the resource file.
	 */
	private int mFieldId = 0;

	/**
	 * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever {@link #mObjects} is modified.
	 */
	private boolean mNotifyOnChange = true;

	private Context mContext;

	// A copy of the original mObjects array, initialized from and then used instead as soon as
	// the mFilter ArrayFilter is used. mObjects will then only contain the filtered values.
	private ArrayList<T> mOriginalValues;
	private ArrayFilter mFilter;

	private LayoutInflater mInflater;

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param textViewResourceId
	 *           The resource ID for a layout file containing a TextView to use when instantiating views.
	 */
	public ArrayAdapterExtended( Context context, int textViewResourceId ) {
		init( context, textViewResourceId, 0, new ArrayList<T>() );
	}

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param resource
	 *           The resource ID for a layout file containing a layout to use when instantiating views.
	 * @param textViewResourceId
	 *           The id of the TextView within the layout resource to be populated
	 */
	public ArrayAdapterExtended( Context context, int resource, int textViewResourceId ) {
		init( context, resource, textViewResourceId, new ArrayList<T>() );
	}

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param textViewResourceId
	 *           The resource ID for a layout file containing a TextView to use when instantiating views.
	 * @param objects
	 *           The objects to represent in the ListView.
	 */
	public ArrayAdapterExtended( Context context, int textViewResourceId, T[] objects ) {
		init( context, textViewResourceId, 0, Arrays.asList( objects ) );
	}

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param resource
	 *           The resource ID for a layout file containing a layout to use when instantiating views.
	 * @param textViewResourceId
	 *           The id of the TextView within the layout resource to be populated
	 * @param objects
	 *           The objects to represent in the ListView.
	 */
	public ArrayAdapterExtended( Context context, int resource, int textViewResourceId, T[] objects ) {
		init( context, resource, textViewResourceId, Arrays.asList( objects ) );
	}

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param textViewResourceId
	 *           The resource ID for a layout file containing a TextView to use when instantiating views.
	 * @param objects
	 *           The objects to represent in the ListView.
	 */
	public ArrayAdapterExtended( Context context, int textViewResourceId, List<T> objects ) {
		init( context, textViewResourceId, 0, objects );
	}

	/**
	 * Constructor
	 * 
	 * @param context
	 *           The current context.
	 * @param resource
	 *           The resource ID for a layout file containing a layout to use when instantiating views.
	 * @param textViewResourceId
	 *           The id of the TextView within the layout resource to be populated
	 * @param objects
	 *           The objects to represent in the ListView.
	 */
	public ArrayAdapterExtended( Context context, int resource, int textViewResourceId, List<T> objects ) {
		init( context, resource, textViewResourceId, objects );
	}

	/**
	 * Adds the specified object at the end of the array.
	 * 
	 * @param object
	 *           The object to add at the end of the array.
	 */
	public void add( T object ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				mOriginalValues.add( object );
			} else {
				mObjects.add( object );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetAdded();
	}

	/**
	 * Adds the specified Collection at the end of the array.
	 * 
	 * @param collection
	 *           The Collection to add at the end of the array.
	 */
	public void addAll( Collection<? extends T> collection ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				mOriginalValues.addAll( collection );
			} else {
				mObjects.addAll( collection );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetAdded();
	}

	/**
	 * Adds the specified items at the end of the array.
	 * 
	 * @param items
	 *           The items to add at the end of the array.
	 */
	public void addAll( T... items ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				Collections.addAll( mOriginalValues, items );
			} else {
				Collections.addAll( mObjects, items );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetAdded();
	}

	/**
	 * Inserts the specified object at the specified index in the array.
	 * 
	 * @param object
	 *           The object to insert into the array.
	 * @param index
	 *           The index at which the object must be inserted.
	 */
	public void insert( T object, int index ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				mOriginalValues.add( index, object );
			} else {
				mObjects.add( index, object );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetChanged();
	}

	/**
	 * Removes the specified object from the array.
	 * 
	 * @param object
	 *           The object to remove.
	 */
	public void remove( T object ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				mOriginalValues.remove( object );
			} else {
				mObjects.remove( object );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetRemoved();
	}

	/**
	 * Remove all elements from the list.
	 */
	public void clear() {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				mOriginalValues.clear();
			} else {
				mObjects.clear();
			}
		}
		if ( mNotifyOnChange ) notifyDataSetChanged();
	}

	/**
	 * Sorts the content of this adapter using the specified comparator.
	 * 
	 * @param comparator
	 *           The comparator used to sort the objects contained in this adapter.
	 */
	public void sort( Comparator<? super T> comparator ) {
		synchronized ( mLock ) {
			if ( mOriginalValues != null ) {
				Collections.sort( mOriginalValues, comparator );
			} else {
				Collections.sort( mObjects, comparator );
			}
		}
		if ( mNotifyOnChange ) notifyDataSetChanged();
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void notifyDataSetChanged() {
		super.notifyDataSetChanged();
		mNotifyOnChange = true;
	}

	/**
	 * Control whether methods that change the list ({@link #add}, {@link #insert}, {@link #remove}, {@link #clear}) automatically
	 * call {@link #notifyDataSetChanged}. If set to false, caller must manually call notifyDataSetChanged() to have the changes
	 * reflected in the attached view.
	 * 
	 * The default is true, and calling notifyDataSetChanged() resets the flag to true.
	 * 
	 * @param notifyOnChange
	 *           if true, modifications to the list will automatically call {@link #notifyDataSetChanged}
	 */
	public void setNotifyOnChange( boolean notifyOnChange ) {
		mNotifyOnChange = notifyOnChange;
	}

	private void init( Context context, int resource, int textViewResourceId, List<T> objects ) {
		mContext = context;
		mInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
		mResource = mDropDownResource = resource;
		mObjects = objects;
		mFieldId = textViewResourceId;
	}

	/**
	 * Returns the context associated with this array adapter. The context is used to create views from the resource passed to the
	 * constructor.
	 * 
	 * @return The Context associated with this adapter.
	 */
	public Context getContext() {
		return mContext;
	}

	/**
	 * {@inheritDoc}
	 */
	public int getCount() {
		return mObjects.size();
	}

	/**
	 * {@inheritDoc}
	 */
	public T getItem( int position ) {
		return mObjects.get( position );
	}

	/**
	 * Returns the position of the specified item in the array.
	 * 
	 * @param item
	 *           The item to retrieve the position of.
	 * 
	 * @return The position of the specified item.
	 */
	public int getPosition( T item ) {
		return mObjects.indexOf( item );
	}

	/**
	 * {@inheritDoc}
	 */
	public long getItemId( int position ) {
		return position;
	}

	/**
	 * {@inheritDoc}
	 */
	public View getView( int position, View convertView, ViewGroup parent ) {
		return createViewFromResource( position, convertView, parent, mResource );
	}

	private View createViewFromResource( int position, View convertView, ViewGroup parent, int resource ) {
		View view;
		TextView text;

		if ( convertView == null ) {
			view = mInflater.inflate( resource, parent, false );
		} else {
			view = convertView;
		}

		try {
			if ( mFieldId == 0 ) {
				// If no custom field is assigned, assume the whole resource is a TextView
				text = (TextView) view;
			} else {
				// Otherwise, find the TextView field within the layout
				text = (TextView) view.findViewById( mFieldId );
			}
		} catch ( ClassCastException e ) {
			Log.e( "ArrayAdapter", "You must supply a resource ID for a TextView" );
			throw new IllegalStateException( "ArrayAdapter requires the resource ID to be a TextView", e );
		}

		T item = getItem( position );
		if ( item instanceof CharSequence ) {
			text.setText( (CharSequence) item );
		} else {
			text.setText( item.toString() );
		}

		return view;
	}

	/**
	 * <p>
	 * Sets the layout resource to create the drop down views.
	 * </p>
	 * 
	 * @param resource
	 *           the layout resource defining the drop down views
	 * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
	 */
	public void setDropDownViewResource( int resource ) {
		this.mDropDownResource = resource;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public View getDropDownView( int position, View convertView, ViewGroup parent ) {
		return createViewFromResource( position, convertView, parent, mDropDownResource );
	}

	/**
	 * Creates a new ArrayAdapter from external resources. The content of the array is obtained through
	 * {@link android.content.res.Resources#getTextArray(int)}.
	 * 
	 * @param context
	 *           The application's environment.
	 * @param textArrayResId
	 *           The identifier of the array to use as the data source.
	 * @param textViewResId
	 *           The identifier of the layout used to create views.
	 * 
	 * @return An ArrayAdapter<CharSequence>.
	 */
	public static ArrayAdapterExtended<CharSequence> createFromResource( Context context, int textArrayResId, int textViewResId ) {
		CharSequence[] strings = context.getResources().getTextArray( textArrayResId );
		return new ArrayAdapterExtended<CharSequence>( context, textViewResId, strings );
	}

	/**
	 * {@inheritDoc}
	 */
	public Filter getFilter() {
		if ( mFilter == null ) {
			mFilter = new ArrayFilter();
		}
		return mFilter;
	}

	/**
	 * <p>
	 * An array filter constrains the content of the array adapter with a prefix. Each item that does not start with the supplied
	 * prefix is removed from the list.
	 * </p>
	 */
	private class ArrayFilter extends Filter {

		@Override
		protected FilterResults performFiltering( CharSequence prefix ) {
			FilterResults results = new FilterResults();

			if ( mOriginalValues == null ) {
				synchronized ( mLock ) {
					mOriginalValues = new ArrayList<T>( mObjects );
				}
			}

			if ( prefix == null || prefix.length() == 0 ) {
				ArrayList<T> list;
				synchronized ( mLock ) {
					list = new ArrayList<T>( mOriginalValues );
				}
				results.values = list;
				results.count = list.size();
			} else {
				String prefixString = prefix.toString().toLowerCase();

				ArrayList<T> values;
				synchronized ( mLock ) {
					values = new ArrayList<T>( mOriginalValues );
				}

				final int count = values.size();
				final ArrayList<T> newValues = new ArrayList<T>();

				for ( int i = 0; i < count; i++ ) {
					final T value = values.get( i );
					final String valueText = value.toString().toLowerCase();

					// First match against the whole, non-splitted value
					if ( valueText.startsWith( prefixString ) ) {
						newValues.add( value );
					} else {
						final String[] words = valueText.split( " " );
						final int wordCount = words.length;

						// Start at index 0, in case valueText starts with space(s)
						for ( int k = 0; k < wordCount; k++ ) {
							if ( words[k].startsWith( prefixString ) ) {
								newValues.add( value );
								break;
							}
						}
					}
				}

				results.values = newValues;
				results.count = newValues.size();
			}

			return results;
		}

		@Override
		protected void publishResults( CharSequence constraint, FilterResults results ) {
			// noinspection unchecked
			mObjects = (List<T>) results.values;
			if ( results.count > 0 ) {
				notifyDataSetChanged();
			} else {
				notifyDataSetInvalidated();
			}
		}
	}

}
